﻿// The Nova Project by Ken Beckett.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

using Nova.CodeDOM;

namespace Nova.UI
{
    /// <summary>
    /// The view model for a <see cref="CodeDOM.Literal"/>.
    /// </summary>
    public class LiteralVM : ExpressionVM
    {
        #region /* STATICS */

        internal static void AddViewModelMapping()
        {
            CreateViewModel.Add(typeof(Literal),
                delegate(CodeObject codeObject, bool isDescription, Dictionary<CodeObject, CodeObjectVM> dictionary) { return new LiteralVM((Literal)codeObject, dictionary); });
        }

        #endregion

        #region /* CONSTRUCTORS */

        /// <summary>
        /// Create a view model instance for the specified <see cref="CodeDOM.Literal"/>.
        /// </summary>
        public LiteralVM(Literal literal, Dictionary<CodeObject, CodeObjectVM> dictionary)
            : base(literal, dictionary)
        { }

        #endregion

        #region /* PROPERTIES */

        /// <summary>
        /// The underlying <see cref="CodeDOM.Literal"/> model.
        /// </summary>
        public Literal Literal
        {
            get { return (Literal)CodeObject; }
        }

        #endregion

        #region /* METHODS */

        #endregion

        #region /* RENDERING */

        /// <summary>
        /// Determines if commas should be displayed in numeric values.
        /// </summary>
        public static bool CommasInNumerics;

        /// <summary>
        /// Determines if actual or alternative symbols should be shown in strings/chars instead of escape sequences.
        /// </summary>
        public static bool NoEscapes;

        /// <summary>
        /// Determines if spaces should be rendered with visible symbols in strings/chars.
        /// </summary>
        public static bool VisibleSpaces;

        /// <summary>
        /// Determines if quotes around strings should be hidden.
        /// </summary>
        public static bool HideQuotes;

        /// <summary>
        /// The font used to render control characters.
        /// </summary>
        public static FontFamily ControlFont = new FontFamily("Arial Unicode MS");

        public override Brush BorderBrush
        {
            get { return Brushes.LightGray; }
        }

        public override Brush BackgroundBrush
        {
            get { return Brushes.White; }
        }

        public override void RenderExpression(CodeRenderer renderer, RenderFlags flags)
        {
            string text = Literal.Text;
            char ch = text[0];
            if (ch == '"' || ch == '@')
                RenderStringOrChar(renderer, text, false, this);
            else if (ch == '\'')
                RenderStringOrChar(renderer, text, true, this);
            else if (char.IsDigit(ch) || (ch == '.') || (ch == '-' && text != "-Infinity") || (ch == '+'))
                RenderNumeric(renderer, text, this);
            else if (text == Literal.ParseTokenNull || text == Literal.ParseTokenTrue || text == Literal.ParseTokenFalse)
                renderer.RenderText(text, KEYWORD_BRUSH, this);
            else if (text == "NaN" || text == "Infinity" || text == "-Infinity")
                renderer.RenderText(text, NORMAL_BRUSH, this);
            else
                renderer.RenderText(text, ERROR_BRUSH, this);
        }

        protected static void RenderStringOrChar(CodeRenderer renderer, string text, bool isChar, CodeObjectVM tag)
        {
            bool isVerbatim = (text[0] == '@');

            // Render escape sequences (and special chars) in a different color, and in red if invalid.
            int start = 0;
            char escCh = '\0', escVal = '\0';
            int escPos = 0, spacePos = 0;
            int openQuotePos = (isVerbatim ? 1 : 0);
            int i;
            for (i = 0; i < text.Length; ++i)
            {
                char ch = text[i];

                // Handle the verbatim marker (if any)
                if (i == 0 && isVerbatim)
                {
                    RenderText(renderer, text, ref start, 1, true, STRING_ESC_BRUSH, tag);
                    continue;
                }

                // Hide the starting double quote if the option is on
                if (ch == '"' && i == openQuotePos)
                {
                    if (HideQuotes)
                        start = openQuotePos + 1;
                    continue;
                }

                // Check if we're in a string of visible spaces
                if (spacePos > 0)
                {
                    if (ch == ' ')
                    {
                        ++spacePos;
                        continue;
                    }

                    // Display visible spaces using an "open box" character
                    renderer.RenderText(new string('\x2423', spacePos), STRING_BRUSH, ControlFont, -2, tag);
                    start += spacePos;
                    spacePos = 0;
                }

                // Check if we're currently inside an escape sequence
                if (escPos > 0)
                {
                    bool processed = true;
                    bool done = false, error = false;
                    int end = i + 1;
                    if (escPos == 1)
                    {
                        if (isVerbatim)
                        {
                            if (ch == '"')
                            {
                                escVal = ch;
                                done = true;
                            }
                            else
                                error = true;
                        }
                        else
                        {
                            escCh = ch;
                            switch (ch)
                            {
                                case '0': done = true; break;
                                case 'a': escVal = '\a'; done = true; break;
                                case 'b': escVal = '\b'; done = true; break;
                                case 'f': escVal = '\f'; done = true; break;
                                case 'n': escVal = '\n'; done = true; break;
                                case 'r': escVal = '\r'; done = true; break;
                                case 't': escVal = '\t'; done = true; break;
                                case 'v': escVal = '\v'; done = true; break;
                                case 'x': case 'u': case 'U':
                                    break;
                                case '\\': case '\'': case '"':
                                    escVal = ch; done = true; break;
                                default: error = true; break;
                            }
                        }
                    }
                    else
                    {
                        // Handle 'x', 'u', and 'U' sequences
                        if (Uri.IsHexDigit(ch))
                        {
                            escVal = (char)((escVal * 16) + Uri.FromHex(ch));
                            done = (escPos == (escCh == 'U' ? 9 : 5));
                        }
                        else
                        {
                            // In this case, don't include the terminating char
                            --end;
                            processed = false;
                            done = true;
                            error = (escPos == 2 || escCh != 'x');
                        }
                    }

                    // Mark '\U' sequence red if in a char
                    if (done && escCh == 'U' && isChar)
                        error = true;

                    if (error)
                    {
                        // Render the entire escape sequence in red if any errors are found
                        RenderText(renderer, text, ref start, end, false, ERROR_BRUSH, tag);
                        escPos = 0;
                    }
                    else if (done)
                    {
                        // Display all escape sequences as actual chars if option is on
                        if (NoEscapes)
                        {
                            // Display control characters as special symbols with 2-3 letter names
                            if (escVal < 0x20 || escVal == 0x7f)
                            {
                                char sym = (escCh == 'n' ? '\u2424' : escVal == 0x7f ? '\u2421' : (char)('\u2400' + escVal));
                                renderer.RenderText(sym.ToString(), STRING_BRUSH, ControlFont, tag);
                            }
                            else
                                renderer.RenderText(escVal.ToString(), STRING_BRUSH, tag);
                            start = end;
                        }
                        // Mark "\'" escapes in strings and '\"' escapes in chars as unnecessary
                        else if ((escVal == '\'' && !isChar) || (escVal == '"' && isChar))
                        {
                            RenderText(renderer, text, ref start, end - 1, true, REDUNDANT_BRUSH, tag);
                            RenderText(renderer, text, ref start, end, true, STRING_BRUSH, tag);
                        }
                        // Display escape sequence
                        else
                            RenderText(renderer, text, ref start, end, true, STRING_ESC_BRUSH, tag);

                        escPos = 0;
                    }
                    else
                        ++escPos;

                    if (processed)
                        continue;
                }

                // Handle escape sequence start
                if ((ch == '\\' && !isVerbatim) || (ch == '"' && isVerbatim))
                {
                    RenderText(renderer, text, ref start, i, true, STRING_BRUSH, tag);
                    escPos = 1;
                    escVal = '\0';
                }
                // Handle newlines in verbatim strings
                else if (ch == '\n')
                {
                    RenderText(renderer, text, ref start, i, true, STRING_BRUSH, tag);
                    ++start;  // Skip over the newline char
                    renderer.NewLine();
                }
                // Handle the start of some visible spaces
                else if (ch == ' ' && VisibleSpaces)
                {
                    RenderText(renderer, text, ref start, i, true, STRING_BRUSH, tag);
                    spacePos = 1;
                }
            }

            // Hide ending double quote if option is on
            // (do this here so quote will flush any active escape sequences)
            if (HideQuotes && text[i - 1] == '"')
                --i;

            // Only cache if less than 16 in length
            RenderText(renderer, text, ref start, i, (i - start < 16), STRING_BRUSH, tag);
        }

        protected static void RenderText(CodeRenderer renderer, string text, ref int start, int end, bool cache, Brush brush, CodeObjectVM tag)
        {
            int length = end - start;
            if (length > 0)
            {
                string partial = text.Substring(start, length);
                start = end;
                if (partial.Length > 0)
                    renderer.RenderText(partial, brush, cache ? TextStyle.Normal : TextStyle.NoCache, tag);
            }
        }

        protected static void RenderNumeric(CodeRenderer renderer, string text, CodeObjectVM tag)
        {
            if (text.StartsWith("0x"))
                renderer.RenderText(text, NUMERIC_BRUSH, tag);
            else
            {
                // Display numerics with commas if enabled
                if (CommasInNumerics)
                {
                    string formatted = text;
                    int count = 0;
                    for (int i = formatted.Length - 1; i > 0; --i)
                    {
                        if (char.IsDigit(formatted, i))
                        {
                            if (++count == 3 && char.IsDigit(formatted, i - 1))
                                formatted = formatted.Insert(i, ",");
                            else
                                continue;
                        }
                        count = 0;
                    }
                    text = formatted;
                }

                // Render the numeric, displaying any alpha chars in a different color (exponent indicator or type suffix)
                bool isAlpha = false;
                int pos, start = 0;
                for (pos = 0; pos < text.Length; ++pos)
                {
                    char ch = text[pos];
                    if (char.IsLetter(ch))
                    {
                        if (!isAlpha)
                        {
                            RenderText(renderer, text, ref start, pos, true, NUMERIC_BRUSH, tag);
                            isAlpha = true;
                        }
                    }
                    else
                    {
                        if (isAlpha)
                        {
                            RenderText(renderer, text, ref start, pos, true, NUMERIC_ALPHA_BRUSH, tag);
                            isAlpha = false;
                        }
                    }
                }
                RenderText(renderer, text, ref start, pos, true, (isAlpha ? NUMERIC_ALPHA_BRUSH : NUMERIC_BRUSH), tag);
            }
        }

        public override void RenderToolTip(CodeRenderer renderer, RenderFlags flags)
        {
            TypeRefBaseVM.RenderType(renderer, CodeObject.GetType(), flags, this);
            renderer.RenderText(" :  ", NORMAL_BRUSH, TextStyle.Proportional | TextStyle.Bold);
            RenderDescription(renderer);
            RenderMessagesInToolTip(renderer, flags);
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand CommasInNumericsCommand = new RoutedCommand("Show Commas In Numerics", typeof(LiteralVM));
        public static readonly RoutedCommand NoEscapesCommand = new RoutedCommand("No Escapes in Strings/Chars", typeof(LiteralVM));
        public static readonly RoutedCommand VisibleSpacesCommand = new RoutedCommand("Visible Spaces in Strings/Chars", typeof(LiteralVM));
        public static readonly RoutedCommand HideQuotesCommand = new RoutedCommand("Hide String Quotes", typeof(LiteralVM));

        public static new void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, NoEscapesCommand, noEscapes_Executed);
            WPFUtil.AddCommandBinding(window, VisibleSpacesCommand, visibleSpaces_Executed);
            WPFUtil.AddCommandBinding(window, HideQuotesCommand, hideQuotes_Executed);
            WPFUtil.AddCommandBinding(window, CommasInNumericsCommand, showCommasInNumerics_Executed);
        }

        private static void showCommasInNumerics_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            CommasInNumerics = !CommasInNumerics;
            CodeRenderer.ReRenderAll();
        }

        private static void noEscapes_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            NoEscapes = !NoEscapes;
            CodeRenderer.ReRenderAll();
        }

        private static void visibleSpaces_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            VisibleSpaces = !VisibleSpaces;
            CodeRenderer.ReRenderAll();
        }

        private static void hideQuotes_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            HideQuotes = !HideQuotes;
            CodeRenderer.ReRenderAll();
        }

        #endregion
    }
}
